package ec.cd.processor;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;

import ec.cd.common.Sensors;
import ec.cd.common.Sensors.SensorData;
import java.io.*;
import java.util.LinkedList;

public class TraceProcessor {

    private List<SensorData> accData; // will hold the raw values from the acc
    private List<SensorData> gyrData; // will hold the raw values from the gyr
    private List<Integer> reportedContextsAcc;
    private List<Integer> reportedContextsGyr;

    public static class ProcessingResults {

        public int numMessagesSent;
        public int numMessagesNovelApproach;
        public int numTotalMessages;
        public int numErrors;
        public double QoI;
    }

    public TraceProcessor() {
        // TODO Auto-generated constructor stub
        accData = new Vector<SensorData>();
        gyrData = new Vector<SensorData>();
        reportedContextsAcc = new Vector<Integer>();
        reportedContextsGyr = new Vector<Integer>();
    }

    public boolean readDataFromFiles(File trace) {
        try {
            FileReader fileReader = new FileReader(trace);
            BufferedReader reader = new BufferedReader(fileReader);
            String line = null;
            while ((line = reader.readLine()) != null) {
                StringTokenizer tokenizer = new StringTokenizer(line, ", ");
                SensorData sensorData = new SensorData();
                sensorData.timestamp = Long.parseLong(tokenizer.nextToken());
                sensorData.sensorType = Integer.parseInt(tokenizer.nextToken());
                sensorData.values = new float[3];
                sensorData.values[0] = Float.parseFloat(tokenizer.nextToken());
                sensorData.values[1] = Float.parseFloat(tokenizer.nextToken());
                sensorData.values[2] = Float.parseFloat(tokenizer.nextToken());
                if (sensorData.sensorType == Sensors.ST_ACCELEROMETER) {
                    accData.add(sensorData);
                } else {
                    gyrData.add(sensorData);
                }
            }
        } catch (IOException ex) {
            ex.printStackTrace();
            return false;
        } catch (Exception ex1) {
            ex1.printStackTrace();
            System.exit(1);
        }
        return true;
    }

    public double[] getAccRanges() {
        List<Double> tiltX = new ArrayList<Double>();
        List<Double> tiltY = new ArrayList<Double>();
        List<Double> tiltZ = new ArrayList<Double>();

        double p1, p2;
        int resMin, resMax;
        try {
            for (Sensors.SensorData sensorData : accData) {
                // Print the content on the console
                double x, y, z, totalAcc,tx,ty,tz;
                x = sensorData.values[0];
                y = sensorData.values[1];
                z = sensorData.values[2];
                totalAcc = Math.sqrt(x * x + y * y + z * z);
                tx = Math.toDegrees(Math.acos(x / totalAcc)); //we consider only this, as the calibration position is the one with all the gravity vector oriented towards X axis
                ty = Math.toDegrees(Math.acos(y / totalAcc));
                tz = Math.toDegrees(Math.acos(z / totalAcc));
                if (y < 0) { //the phone has gone around
                    tx = - tx;
                }
                tiltX.add(tx);
                tiltY.add(ty);
                tiltZ.add(tz);
            }
            //using percentile as a way of removing errors and getting relevant ranges
            p1 = 0.05;
            p2 = 0.95;
            Collections.sort(tiltX);
            Collections.sort(tiltY);
            Collections.sort(tiltZ);
            resMin = (int) Math.round((p1 * tiltX.size()) + 1 / 2f);
            resMax = (int) Math.round((p2 * tiltX.size()) + 1 / 2f);
            double[] ranges = new double[6];
            ranges[0] = tiltX.get(resMin - 1);
            ranges[1] = tiltX.get(resMax - 1);
            ranges[2] = tiltY.get(resMin - 1);
            ranges[3] = tiltY.get(resMax - 1);
            ranges[4] = tiltZ.get(resMin - 1);
            ranges[5] = tiltZ.get(resMax - 1);
            return ranges;
        } catch (Exception e) {// Catch exception if any
            System.err.println("Error in calibration: " + e.getMessage());
            e.printStackTrace();
            return null;
        }
    }

    public double[] getRangesGyro() {
        List<Double> gyrX = new ArrayList<Double>();
        List<Double> gyrY = new ArrayList<Double>();
        List<Double> gyrZ = new ArrayList<Double>();
        double p1, p2;
        int resMin, resMax;
        try {
            for (Sensors.SensorData sensorData : gyrData) {
                gyrX.add((double) sensorData.values[0]);
                gyrY.add((double) sensorData.values[1]);
                gyrZ.add((double) sensorData.values[2]);
            }

            p1 = 0.05;
            p2 = 0.95;
            Collections.sort(gyrX);
            Collections.sort(gyrY);
            Collections.sort(gyrZ);

            resMin = (int) Math.round((p1 * gyrX.size()) + 1 / 2f);
            resMax = (int) Math.round((p2 * gyrX.size()) + 1 / 2f);
            double[] ranges = new double[6];
            ranges[0] = gyrX.get(resMin - 1);
            ranges[1] = gyrX.get(resMax - 1);
            ranges[2] = gyrY.get(resMin - 1);
            ranges[3] = gyrY.get(resMax - 1);
            ranges[4] = gyrZ.get(resMin - 1);
            ranges[5] = gyrZ.get(resMax - 1);
            return ranges;
        } catch (Exception e) {// Catch exception if any
            System.err.println("Error in calibration: " + e.getMessage());
            e.printStackTrace();
            return null;
        }
    }

    /*
     * @param q The tolerance range. It should generally be a number between 0
     * and 1.
     *
     * @param ranges The ranges for identifying the context. {minSitting,
     * maxSitting, minWalking, maxWalking, minRunning, maxRuning} These are the
     * ranges just for X values of the sensor, be it acc or gyro. For acc it is
     * the tiltX ranges
     *
     * @param indexToCheck This will tell the algorithm to check X,Y or Z
     * against the ranges. 0, 1, 2 respectively
     */
    public ProcessingResults getProcessingResults(
            List<SensorData> sensorDatas, double q, double[] ranges,
            int indexToCheck) {
        String sensorType = "";
        if (sensorDatas.isEmpty()) {
            return null;
        } else {
            double minSitting = ranges[0], maxSitting = ranges[1],
                    minWalking = ranges[2], maxWalking = ranges[3],
                    minRunning = ranges[4], maxRunning = ranges[5];
            int numMessagesToBeSent = 0, numMessagesToBeSentNovelApproach = 0;
            int numErrors = 0;
            int lastReportedContext = 0, currentContext = 0, previousContext = 0;
            float lastReportedValue = 0;
            List<SensorData> sensorDataCalc = new LinkedList<SensorData>();
            if (sensorDatas.get(1).sensorType == Sensors.ST_ACCELEROMETER) {
                // transform raw data into tilt data
                sensorType = "Accelerometer";
                for (Sensors.SensorData sensorData : sensorDatas) {
                    SensorData temp = new SensorData();
                    temp.sensorType = sensorData.sensorType;
                    temp.timestamp = sensorData.timestamp;
                    temp.values = new float[3];
                    double x, y, z, totalAcc, tiltX, tiltY, tiltZ;
                    x = sensorData.values[0];
                    y = sensorData.values[1];
                    z = sensorData.values[2];
                    totalAcc = Math.sqrt(x * x + y * y + z * z);
                    tiltX = Math.toDegrees(Math.acos(x / totalAcc));
                    tiltY = Math.toDegrees(Math.acos(y / totalAcc));
                    tiltZ = Math.toDegrees(Math.acos(z / totalAcc));
                    if (y < 0) {
                        tiltX = - tiltX;
                    }
                    //System.out.println(Math.toDegrees(tiltX));
                    //System.out.println(Math.toDegrees(tiltX));
                    //System.out.println(Math.toDegrees(tiltX));
                    temp.values[0] = (float) tiltX;
                    temp.values[1] = (float) tiltY;
                    temp.values[2] = (float) tiltZ;
                    sensorDataCalc.add(temp);
                }
            } else {
                sensorType = "Gyroscope";
                for (Sensors.SensorData sensorData : sensorDatas) {
                    SensorData temp = new SensorData();
                    temp.sensorType = sensorData.sensorType;
                    temp.timestamp = sensorData.timestamp;
                    temp.values = new float[3];
                    temp.values[0] = sensorData.values[0];
                    temp.values[1] = sensorData.values[1];
                    temp.values[2] = sensorData.values[2];
                    sensorDataCalc.add(temp);
                }
            }
            for (SensorData sensorData : sensorDataCalc) {

                float[] sensorVal = sensorData.values;
                // considering only X values
                if (sensorVal[indexToCheck] >= minSitting
                        && sensorVal[indexToCheck] <= maxSitting) {
                    currentContext = 1;
                } else if (sensorVal[indexToCheck] >= minWalking
                        && sensorVal[indexToCheck] <= maxWalking) {
                    currentContext = 2;
                } else if (sensorVal[indexToCheck] >= minRunning
                        && sensorVal[indexToCheck] <= maxRunning) {
                    currentContext = 3;
                } else {
                    currentContext = 0;
                }
                
                //measure number of messages to be sent only in case the values cross context
                if(currentContext != previousContext){
                    numMessagesToBeSentNovelApproach +=1;
                }
                previousContext = currentContext;
                
                //check if the value is to be sent in the case with tolerance ranges.
                if (Math.abs(sensorVal[indexToCheck] - lastReportedValue)
                        >= Math.abs(q * lastReportedValue)) {
                    // the value is to be reported
                    numMessagesToBeSent += 1;
                    lastReportedContext = currentContext;
                    lastReportedValue = sensorVal[indexToCheck];
                } else {
                    //the value will not be reported, lets check for error
                    if (currentContext != lastReportedContext) { //it is error only when the server knows different context from the truth
                        numErrors += 1;
                    }
                }


                if (sensorData.sensorType == Sensors.ST_ACCELEROMETER) {
                    reportedContextsAcc.add(lastReportedContext);
                } else {
                    reportedContextsGyr.add(lastReportedContext);
                }
            }

            ProcessingResults processingResults = new ProcessingResults();
            processingResults.numMessagesSent = numMessagesToBeSent;
            processingResults.numTotalMessages = sensorDatas.size();
            processingResults.numErrors = numErrors;
            System.out.println("Number of messages in total: " + sensorDatas.size());
            System.out.println("Number of messages to be sent: " + numMessagesToBeSent);
            System.out.println("Errors: " + numErrors);
            processingResults.QoI = 1 - (numErrors / (double) sensorDatas.size());
            processingResults.numMessagesNovelApproach = numMessagesToBeSentNovelApproach;
            try {
                writeProcessingResultsToFile(sensorType, q, processingResults);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return processingResults;
        }
    }

    public static void tilt(double x, double y, double z) {
        double totalAcc = Math.sqrt(x * x + y * y + z * z);
        double tiltX = Math.toDegrees(Math.acos(x / totalAcc));
        double tiltY = Math.toDegrees(Math.acos(y / totalAcc));
        double tiltZ = Math.toDegrees(Math.acos(z / totalAcc));
        System.out.println(tiltX);
        System.out.println(tiltY);
        System.out.println(tiltZ);
    }

    protected void writeProcessingResultsToFile(String sensorName, double q, ProcessingResults processingResults) throws IOException {
        File file = new File("results" + sensorName + ".txt");
        if (!file.exists()) {
            file.createNewFile();
        }
        FileWriter fileWriter = new FileWriter(file, true);
        BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
        bufferedWriter.write(q + " " + processingResults.numTotalMessages + " " + processingResults.numMessagesSent + " " + processingResults.numErrors + " " + processingResults.QoI + " " + processingResults.numMessagesNovelApproach);
        bufferedWriter.newLine();
        bufferedWriter.close();
    }

    public static void main(String args[]) {
        TraceProcessor.tilt(0.324322, -0.653423, 0.684234);
    }

    public List<SensorData> getAccData() {
        return accData;
    }

    public void setAccData(List<SensorData> accData) {
        this.accData = accData;
    }

    public List<SensorData> getGyrData() {
        return gyrData;
    }

    public void setGyrData(List<SensorData> gyrData) {
        this.gyrData = gyrData;
    }

    public List<Integer> getReportedContextsAcc() {
        return reportedContextsAcc;
    }

    public void setReportedContextsAcc(List<Integer> reportedContextsAcc) {
        this.reportedContextsAcc = reportedContextsAcc;
    }

    public List<Integer> getReportedContextsGyr() {
        return reportedContextsGyr;
    }

    public void setReportedContextsGyr(List<Integer> reportedContextsGyr) {
        this.reportedContextsGyr = reportedContextsGyr;
    }
}
